package com.bird.component.jackson

import com.bird.component.lang.util.isnull
import com.bird.component.lang.util.nonnull
import com.fasterxml.jackson.annotation.JsonAutoDetect
import com.fasterxml.jackson.annotation.PropertyAccessor
import com.fasterxml.jackson.databind.Module
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.PropertyNamingStrategy
import com.fasterxml.jackson.databind.SerializationFeature
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer
import org.springframework.beans.BeanUtils
import org.springframework.beans.factory.BeanFactoryUtils
import org.springframework.beans.factory.ListableBeanFactory
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer
import org.springframework.boot.autoconfigure.jackson.JacksonProperties
import org.springframework.context.ApplicationContext
import org.springframework.core.Ordered
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder
import org.springframework.util.Assert
import org.springframework.util.ClassUtils
import org.springframework.util.ReflectionUtils
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.time.format.DateTimeFormatter
import java.util.*

class MeditateJackson2ObjectMapperBuilderCustomizer(
    val applicationContext: ApplicationContext,
    val jacksonProperties: JacksonProperties
) : Jackson2ObjectMapperBuilderCustomizer, Ordered {
    companion object {
        private val dateFormat: String = "yyyy-MM-dd"
        private val dateTimeFormat: String = "yyyy-MM-dd HH:mm:ss"
        private val timeFormat: String = "HH:mm:ss"
        private val FEATURE_DEFAULTS: Map<Any, Boolean>

        init {
            val featureDefaults = HashMap<Any, Boolean>()
            featureDefaults.put(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
            FEATURE_DEFAULTS = Collections.unmodifiableMap(featureDefaults)
        }

    }

    override fun getOrder(): Int {
        return 0
    }

    override fun customize(builder: Jackson2ObjectMapperBuilder) {
        if (jacksonProperties.defaultPropertyInclusion.nonnull()) {
            builder.serializationInclusion(jacksonProperties.defaultPropertyInclusion)
        }

        if (jacksonProperties.timeZone.nonnull()) {
            builder.timeZone(jacksonProperties.timeZone)
        }
        builder.simpleDateFormat(dateFormat)
        builder.serializers(LocalDateSerializer(DateTimeFormatter.ofPattern(dateFormat)))
        builder.serializers(LocalDateTimeSerializer(DateTimeFormatter.ofPattern(dateTimeFormat)))
        builder.serializers(LocalTimeSerializer(DateTimeFormatter.ofPattern(timeFormat)))
        builder.deserializers(LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(dateTimeFormat)))
        builder.deserializers(LocalDateDeserializer(DateTimeFormatter.ofPattern(dateFormat)))
        builder.deserializers(LocalTimeDeserializer(DateTimeFormatter.ofPattern(timeFormat)))
        this.configureFeatures(
            builder,
            FEATURE_DEFAULTS
        )
        configureVisibility(builder, jacksonProperties.visibility)
        configureFeatures(builder, jacksonProperties.deserialization)
        configureFeatures(builder, jacksonProperties.serialization)
        configureFeatures(builder, jacksonProperties.mapper)
        configureFeatures(builder, jacksonProperties.parser)
        configureFeatures(builder, jacksonProperties.generator)
        configureDateFormat(builder)
        configurePropertyNamingStrategy(builder)
        configureModules(builder)
        configureLocale(builder)
    }

    private fun configureFeatures(builder: Jackson2ObjectMapperBuilder, features: Map<*, Boolean>) {
        features.forEach { (feature: Any?, value: Boolean) ->
            if (value) {
                builder.featuresToEnable(*arrayOf(feature))
            } else {
                builder.featuresToDisable(*arrayOf(feature))
            }
        }
    }

    private fun configureVisibility(
        builder: Jackson2ObjectMapperBuilder,
        visibilities: Map<PropertyAccessor, JsonAutoDetect.Visibility>
    ) {
        visibilities.forEach { (accessor: PropertyAccessor, visibility: JsonAutoDetect.Visibility) ->
            builder.visibility(
                accessor,
                visibility
            )
        }
    }

    private fun configureDateFormat(builder: Jackson2ObjectMapperBuilder) {
        val dateFormat:String? = jacksonProperties.dateFormat
        if (dateFormat.nonnull()) {
            try {
                val dateFormatClass = dateFormat?.let { ClassUtils.forName(it, null as ClassLoader?) }
                builder.dateFormat(BeanUtils.instantiateClass(dateFormatClass) as DateFormat)
            } catch (var6: ClassNotFoundException) {
                val simpleDateFormat = dateFormat?.let { SimpleDateFormat(it) }
                var timeZone:TimeZone? = jacksonProperties.timeZone
                if (timeZone.isnull()) {
                    timeZone = ObjectMapper().serializationConfig.timeZone
                }
                simpleDateFormat?.timeZone = timeZone
                simpleDateFormat?.let { builder.dateFormat(it) }
            }
        }
    }

    private fun configurePropertyNamingStrategy(builder: Jackson2ObjectMapperBuilder) {
        val strategy = jacksonProperties.propertyNamingStrategy
        strategy?.let {
            try {
                this.configurePropertyNamingStrategyClass(builder, ClassUtils.forName(it, null as ClassLoader?))
            } catch (e: ClassNotFoundException) {
                this.configurePropertyNamingStrategyField(builder, it)
            }
        }
    }

    private fun configurePropertyNamingStrategyClass(
        builder: Jackson2ObjectMapperBuilder,
        propertyNamingStrategyClass: Class<*>
    ) {
        builder.propertyNamingStrategy(BeanUtils.instantiateClass(propertyNamingStrategyClass) as PropertyNamingStrategy)
    }

    private fun configurePropertyNamingStrategyField(builder: Jackson2ObjectMapperBuilder, fieldName: String) {
        val field = ReflectionUtils.findField(
            PropertyNamingStrategy::class.java, fieldName,
            PropertyNamingStrategy::class.java
        )
        Assert.notNull(
            field
        ) { "Constant named '" + fieldName + "' not found on " + PropertyNamingStrategy::class.java.name }

        try {
            builder.propertyNamingStrategy(field?.get(null as Any?) as PropertyNamingStrategy)
        } catch (e: Exception) {
            throw IllegalStateException(e)
        }
    }

    private fun configureModules(builder: Jackson2ObjectMapperBuilder) {
        val moduleBeans = getBeans(
            this.applicationContext,
            Module::class.java
        )
        builder.modulesToInstall(*moduleBeans.toTypedArray<Module>())
    }

    private fun configureLocale(builder: Jackson2ObjectMapperBuilder) {
        val locale = jacksonProperties.locale
        locale?.let { builder.locale(it) }
    }

    private fun <T> getBeans(beanFactory: ListableBeanFactory, type: Class<T>): Collection<T> {
        return BeanFactoryUtils.beansOfTypeIncludingAncestors(beanFactory, type).values
    }


}